#include "Camera.h"

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include<glm/glm.hpp>
#include<glm/gtc/matrix_transform.hpp>
#include<glm/gtc/type_ptr.hpp>
#include<glm/gtx/rotate_vector.hpp>
#include<glm/gtx/vector_angle.hpp>

namespace OCPP
{
	Camera::Camera()
	{
		
	}

	Camera::~Camera()
	{

	}

	RealRect calcProjectionParameters(bool orthographic = false)
	{
		if (orthographic)
		{
			// Unknown how to apply frustum offset to orthographic camera, just ignore here
			float half_w = 70 * 0.5f;
			float half_h = 70 * 0.5f;

			return RealRect(-half_w, +half_h, +half_w, -half_h);
		}
		float mAspect(1.0);
		float mFarDist(100.0f);
		float mNearDist(0.1f);
		float mFocalLength(1.0f);
		Vector2 mFrustumOffset(Vector2::ZERO);

		Radian mFOVy(Math::PI / 4.0f);

		Radian thetaY(mFOVy * 0.5f);
		float tanThetaY = Math::Tan(thetaY);
		float tanThetaX = tanThetaY * mAspect;

		float nearFocal = mNearDist / mFocalLength;
		float nearOffsetX = mFrustumOffset.x * nearFocal;
		float nearOffsetY = mFrustumOffset.y * nearFocal;
		float half_w = tanThetaX * mNearDist;
		float half_h = tanThetaY * mNearDist;

		return RealRect(-half_w + nearOffsetX, +half_h + nearOffsetY,
			+half_w + nearOffsetX, -half_h + nearOffsetY);
	}

	const Matrix4& Camera::getOthographicProjectMatrix()
	{
		RealRect rect = calcProjectionParameters(true);
		float left = rect.left, right = rect.right, top = rect.top, bottom = rect.bottom;

		// The code below will dealing with general projection
		// parameters, similar glOrtho.
		// Doesn't optimise manually except division operator, so the
		// code more self-explaining.

		float inv_w = 1 / (right - left);
		float inv_h = 1 / (top - bottom);
		float inv_d = 1 / (mFarDist - mNearDist);

		float A = 2 * inv_w;
		float B = 2 * inv_h;
		float C = -(right + left) * inv_w;
		float D = -(top + bottom) * inv_h;
		float q, qn;
		if (mFarDist == 0)
		{
			float INFINITE_FAR_PLANE_ADJUST = 0.00001;

			// Can not do infinite far plane here, avoid divided zero only
			q = -INFINITE_FAR_PLANE_ADJUST / mNearDist;
			qn = -INFINITE_FAR_PLANE_ADJUST - 1;
		}
		else
		{
			q = -2 * inv_d;
			qn = -(mFarDist + mNearDist) * inv_d;
		}

		// NB: This creates 'uniform' orthographic projection matrix,
		// which depth range [-1,1], right-handed rules
		//
		// [ A   0   0   C  ]
		// [ 0   B   0   D  ]
		// [ 0   0   q   qn ]
		// [ 0   0   0   1  ]
		//
		// A = 2 * / (right - left)
		// B = 2 * / (top - bottom)
		// C = - (right + left) / (right - left)
		// D = - (top + bottom) / (top - bottom)
		// q = - 2 / (far - near)
		// qn = - (far + near) / (far - near)

		mProjMatrix = Matrix4::ZERO;
		mProjMatrix[0][0] = A;
		mProjMatrix[0][3] = C;
		mProjMatrix[1][1] = B;
		mProjMatrix[1][3] = D;
		mProjMatrix[2][2] = q;
		mProjMatrix[2][3] = qn;
		mProjMatrix[3][3] = 1;

		return mProjMatrix;
	}

	const Matrix4& Camera::getViewMatrix()
	{
		Vector3 mYawFixedAxis = Vector3::UNIT_Y;
		Vector3 vec = mvOrientation - mvPosition;
		Vector3 zAdjustVec = -vec;
		zAdjustVec.normalise();

		Quaternion targetWorldOrientation;
		targetWorldOrientation = Math::lookRotation(zAdjustVec, mYawFixedAxis);

		mViewMatrix = Math::makeViewMatrix(mvPosition, targetWorldOrientation);

		return mViewMatrix;
	}

	const Matrix4& Camera::getProjectMatrix()
	{
		RealRect rect = calcProjectionParameters();
		float left = rect.left, right = rect.right, top = rect.top, bottom = rect.bottom;
		mProjMatrix = Math::makePerspectiveMatrix(left, right, bottom, top, 0.1f, 100.0f);
		return mProjMatrix;
	}

	const Matrix4& Camera::getViewProjectMatrix()
	{
		mViewProjectMatrix = getProjectMatrix() * getViewMatrix();
		return mViewProjectMatrix;
	}

	void Camera::Inputs(GLFWwindow* window)
	{
		// Handles key inputs
		if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
		{
			mvPosition += speed * mvOrientation;
		}
		if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
		{
			mvPosition += speed * -mvOrientation.crossProduct(mvUp).normalisedCopy();
		}
		if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
		{
			mvPosition += speed * -mvOrientation;
		}
		if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
		{
			mvPosition += speed * mvOrientation.crossProduct(mvUp).normalisedCopy();
		}
		if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS)
		{
			mvPosition += speed * mvUp;
		}
		if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS)
		{
			mvPosition += speed * -mvUp;
		}
		if (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS)
		{
			speed = 0.4f;
		}
		else if (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_RELEASE)
		{
			speed = 0.1f;
		}


		// Handles mouse inputs
		if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS)
		{
			// Hides mouse cursor
			glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN);

			// Prevents camera from jumping on the first click
			if (firstClick)
			{
				glfwSetCursorPos(window, (width / 2), (height / 2));
				firstClick = false;
			}

			// Stores the coordinates of the cursor
			double mouseX;
			double mouseY;
			// Fetches the coordinates of the cursor
			glfwGetCursorPos(window, &mouseX, &mouseY);

			// Normalizes and shifts the coordinates of the cursor such that they begin in the middle of the screen
			// and then "transforms" them into degrees 
			float rotX = sensitivity * (float)(mouseY - (height / 2)) / height;
			float rotY = sensitivity * (float)(mouseX - (width / 2)) / width;

			// Calculates upcoming vertical change in the Orientation
			glm::vec3 newOrientation = glm::rotate(Orientation, glm::radians(-rotX), glm::normalize(glm::cross(Orientation, glm::vec3(mvUp.x, mvUp.y, mvUp.z))));

			// Decides whether or not the next vertical Orientation is legal or not
			if (abs(glm::angle(newOrientation, glm::vec3(mvUp.x, mvUp.y, mvUp.z)) - glm::radians(90.0f)) <= glm::radians(85.0f))
			{
				Orientation = newOrientation;
			}

			// Rotates the Orientation left and right
			Orientation = glm::rotate(Orientation, glm::radians(-rotY), glm::vec3(mvUp.x, mvUp.y, mvUp.z));

			mvOrientation.x = Orientation.x;
			mvOrientation.y = Orientation.y;
			mvOrientation.z = Orientation.z;

			// Sets mouse cursor to the middle of the screen so that it doesn't end up roaming around
			glfwSetCursorPos(window, (width / 2), (height / 2));
		}
		else if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_RELEASE)
		{
			// Unhides cursor since camera is not looking around anymore
			glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
			// Makes sure the next time the camera looks around it doesn't jump
			firstClick = true;
		}
	}
}